[AWS CDK] クロススタック参照で、参照先から参照を削除する場合は、参照元で exportValue を使おう
こんにちは、CX事業本部 Delivery部の若槻です。
今回は、AWS CDK のクロススタック参照で、「参照先での参照(エクスポートの使用)」を削除する方法を確認してみました。
先にまとめ
- 参照先での参照を削除する場合は、参照元で exportValue を使って、エクスポートの存在を保証する必要がある
- 参照先での参照と、参照元でのエクスポートを、1度のデプロイで削除することは出来ない
試してみた
次のような参照元スタックと参照先スタックを作成し、試してみます。
- CDK App
import * as cdk from 'aws-cdk-lib'; import { CdkSampleStack } from '../lib/cdk-sample-app'; import { CdkSampleStack2 } from '../lib/cdk-sample-app-2'; const app = new cdk.App(); // 参照元スタック作成 const cdkSampleStack = new CdkSampleStack(app, 'CdkSampleStack', {}); // 参照先スタック作成 new CdkSampleStack2(app, 'CdkSampleStack2', { sampleParameter: cdkSampleStack._sampleParameter, });
- 参照元スタック
CdkSampleStack
import { aws_ssm, Stack, StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; // 参照元スタック export class CdkSampleStack extends Stack { public readonly _sampleParameter: aws_ssm.StringParameter; constructor(scope: Construct, id: string, props: StackProps) { super(scope, id, props); const sampleParameter = new aws_ssm.StringParameter( this, 'SampleParameter', { parameterName: 'SampleParameter', stringValue: 'SampleValue', } ); this._sampleParameter = sampleParameter; } }
- 参照先スタック
CdkSampleStack2
import { aws_ssm, aws_lambda_nodejs, Stack, StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; interface CdkSampleStack2Props extends StackProps { sampleParameter: aws_ssm.StringParameter; } // 参照先スタック export class CdkSampleStack2 extends Stack { constructor(scope: Construct, id: string, props: CdkSampleStack2Props) { super(scope, id, props); const sampleFunction = new aws_lambda_nodejs.NodejsFunction( this, 'SampleFunction', { entry: 'src/sample-function.ts', environment: { SAMPLE_PARAMETER_NAME: props.sampleParameter.parameterName, }, } ); props.sampleParameter.grantRead(sampleFunction); } }
上記の CDK App をデプロイします。
デプロイ後には Export が作成されていることが確認できます。
$ aws cloudformation list-exports \ --query "Exports[?contains(ExportingStackId,'CdkSampleStack')]" [ { "ExportingStackId": "arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/CdkSampleStack/e68b6900-f01c-11ed-8538-06bbc29e5367", "Name": "CdkSampleStack:ExportsOutputRefSampleParameterEC52094C22ED334E", "Value": "SampleParameter" } ]
1. exportValue を使わない場合、参照先でのエクスポートの使用を削除できない
参照先スタックで、インポートして使用している参照を削除します。
// 参照先スタック export class CdkSampleStack2 extends Stack { constructor(scope: Construct, id: string, props: CdkSampleStack2Props) { super(scope, id, props); // const sampleFunction = new aws_lambda_nodejs.NodejsFunction( // this, // 'SampleFunction', // { // entry: 'src/sample-function.ts', // environment: { // SAMPLE_PARAMETER_NAME: props.sampleParameter.parameterName, // }, // } // ); // props.sampleParameter.grantRead(sampleFunction); } }
cdk deploy '*'
コマンドを実行してデプロイをしようとするとエラーが発生し失敗しました。ここでデプロイが先に走っているのは参照元スタック CdkSampleStack です。
$ cdk deploy '*' Synthesis time: 3.06s CdkSampleStack: start: Building e729aa46a22d433584a5885847489098cb0505e19d9ddee61aa310b08456b63c:current_account-current_region CdkSampleStack: success: Built e729aa46a22d433584a5885847489098cb0505e19d9ddee61aa310b08456b63c:current_account-current_region CdkSampleStack2: start: Building 2823a2fbf0949d41981e8bdbe73ebddaa74da24257a897be46ba45c3646a911b:current_account-current_region CdkSampleStack2: success: Built 2823a2fbf0949d41981e8bdbe73ebddaa74da24257a897be46ba45c3646a911b:current_account-current_region CdkSampleStack: start: Publishing e729aa46a22d433584a5885847489098cb0505e19d9ddee61aa310b08456b63c:current_account-current_region CdkSampleStack: success: Published e729aa46a22d433584a5885847489098cb0505e19d9ddee61aa310b08456b63c:current_account-current_region CdkSampleStack CdkSampleStack2: start: Publishing 2823a2fbf0949d41981e8bdbe73ebddaa74da24257a897be46ba45c3646a911b:current_account-current_region CdkSampleStack: deploying... [1/2] CdkSampleStack2: success: Published 2823a2fbf0949d41981e8bdbe73ebddaa74da24257a897be46ba45c3646a911b:current_account-current_region CdkSampleStack: creating CloudFormation changeset... CdkSampleStack failed: Error: The stack named CdkSampleStack failed to deploy: UPDATE_ROLLBACK_COMPLETE at FullCloudFormationDeployment.monitorDeployment (/Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:397:10236) at processTicksAndRejections (node:internal/process/task_queues:96:5) at async Object.deployStack2 [as deployStack] (/Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:400:149977) at async /Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:400:135508 Deployment failed: Error: The stack named CdkSampleStack failed to deploy: UPDATE_ROLLBACK_COMPLETE at FullCloudFormationDeployment.monitorDeployment (/Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:397:10236) at processTicksAndRejections (node:internal/process/task_queues:96:5) at async Object.deployStack2 [as deployStack] (/Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:400:149977) at async /Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:400:135508 The stack named CdkSampleStack failed to deploy: UPDATE_ROLLBACK_COMPLETE
CloudFormation のスタックのイベントを確認してみると、参照元スタックで Export <参照先スタック名>:<エクスポート名> cannot be deleted as it is in use by <参照元スタック名>
というエラーが発生しています。
Export CdkSampleStack:ExportsOutputRefSampleParameterEC52094C22ED334E cannot be deleted as it is in use by CdkSampleStack2
ここで CDK Diff コマンドで、デプロイしようとしているスタックと、デプロイ済みのスタックの差分を確認してみると、参照先のリソースだけでなく、参照元スタックのエクスポートまで削除されていることがわかります。
$ cdk diff Stack CdkSampleStack Outputs [-] Output ExportsOutputRefSampleParameterEC52094C22ED334E: {"Value":{"Ref":"SampleParameterEC52094C"},"Export":{"Name":"CdkSampleStack:ExportsOutputRefSampleParameterEC52094C22ED334E"}} Stack CdkSampleStack2 IAM Statement Changes ┌───┬─────────────────────────────────────────────────────────────────────────────────────────────────────────┬────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────┬───────────────────────────────────┬───────────┐ │ │ Resource │ Effect │ Action │ Principal │ Condition │ ├───┼─────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────┼───────────┤ │ - │ arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/{"Fn::ImportValue":"CdkSampleStack │ Allow │ ssm:DescribeParameters │ AWS:${SampleFunction/ServiceRole} │ │ │ │ :ExportsOutputRefSampleParameterEC52094C22ED334E"} │ │ ssm:GetParameter │ │ │ │ │ │ │ ssm:GetParameterHistory │ │ │ │ │ │ │ ssm:GetParameters │ │ │ └───┴─────────────────────────────────────────────────────────────────────────────────────────────────────────┴────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────┴───────────────────────────────────┴───────────┘ (NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299) Resources [-] AWS::IAM::Role SampleFunction/ServiceRole SampleFunctionServiceRoleE420739A destroy [-] AWS::IAM::Policy SampleFunction/ServiceRole/DefaultPolicy SampleFunctionServiceRoleDefaultPolicy49A1212C destroy [-] AWS::Lambda::Function SampleFunction SampleFunction7DB1D36A destroy
2. exportValue を使って、参照先でのエクスポートの使用を削除できる
参照先での参照を削除したい場合は、参照元スタックで exportValue を使う必要があります。
参照先で使用する情報(ここではパラメーター名)をエクスポートする記述を追加します。
// 参照元スタック export class CdkSampleStack extends Stack { public readonly _sampleParameter: aws_ssm.StringParameter; constructor(scope: Construct, id: string, props: StackProps) { super(scope, id, props); const sampleParameter = new aws_ssm.StringParameter( this, 'SampleParameter', { parameterName: 'SampleParameter', stringValue: 'SampleValue', } ); this._sampleParameter = sampleParameter; this.exportValue(sampleParameter.parameterName); // exportValue を追加 } }
差分を確認してみると、参照元スタックのエクスポートは削除されていないことがわかります。
$ cdk diff Stack CdkSampleStack There were no differences Stack CdkSampleStack2 IAM Statement Changes ┌───┬─────────────────────────────────────────────────────────────────────────────────────────────────────────┬────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────┬───────────────────────────────────┬───────────┐ │ │ Resource │ Effect │ Action │ Principal │ Condition │ ├───┼─────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────┼───────────┤ │ - │ arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/{"Fn::ImportValue":"CdkSampleStack │ Allow │ ssm:DescribeParameters │ AWS:${SampleFunction/ServiceRole} │ │ │ │ :ExportsOutputRefSampleParameterEC52094C22ED334E"} │ │ ssm:GetParameter │ │ │ │ │ │ │ ssm:GetParameterHistory │ │ │ │ │ │ │ ssm:GetParameters │ │ │ └───┴─────────────────────────────────────────────────────────────────────────────────────────────────────────┴────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────┴───────────────────────────────────┴───────────┘ (NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299) Resources [-] AWS::IAM::Role SampleFunction/ServiceRole SampleFunctionServiceRoleE420739A destroy [-] AWS::IAM::Policy SampleFunction/ServiceRole/DefaultPolicy SampleFunctionServiceRoleDefaultPolicy49A1212C destroy [-] AWS::Lambda::Function SampleFunction SampleFunction7DB1D36A destroy
CDK デプロイをすると、参照先でのエクスポートの使用を削除できました。
$ cdk deploy '*' Synthesis time: 2.77s CdkSampleStack2 CdkSampleStack: start: Building d832fc04e7acebd774869f97322f00940219322e638150f74ae23396ee3e3e5e:current_account-current_region CdkSampleStack: success: Built d832fc04e7acebd774869f97322f00940219322e638150f74ae23396ee3e3e5e:current_account-current_region CdkSampleStack: start: Publishing d832fc04e7acebd774869f97322f00940219322e638150f74ae23396ee3e3e5e:current_account-current_region CdkSampleStack2: deploying... [2/2] CdkSampleStack2: creating CloudFormation changeset... CdkSampleStack: success: Published d832fc04e7acebd774869f97322f00940219322e638150f74ae23396ee3e3e5e:current_account-current_region CdkSampleStack2 Deployment time: 27.65s Stack ARN: arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/CdkSampleStack2/87605200-0206-11ee-b170-0aa1384f745b Total time: 30.42s CdkSampleStack CdkSampleStack: deploying... [1/2] CdkSampleStack: creating CloudFormation changeset... CdkSampleStack Deployment time: 17.32s Outputs: CdkSampleStack.ExportsOutputRefSampleParameterEC52094C22ED334E = SampleParameter Stack ARN: arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/CdkSampleStack/e68b6900-f01c-11ed-8538-06bbc29e5367 Total time: 20.09s
これであとは参照元のエクスポートを削除するなり、残したまま別のスタックでの参照に使用するなり出来ます。
3. 参照先での参照と、参照元のエクスポートを、1回のデプロイで削除できるのか?(できなかった)
ここで、参照先での参照と、参照元のエクスポートを、1回のデプロイで削除できるのか、気になったので試してみました。
一度、参照先での参照と、参照元のエクスポートをどちらも作成し直した上で、いずれも削除(コメントアウトされたコード部分)します。
- CDK App
import * as cdk from 'aws-cdk-lib'; import { CdkSampleStack } from '../lib/cdk-sample-app'; import { CdkSampleStack2 } from '../lib/cdk-sample-app-2'; const app = new cdk.App(); // 参照元スタック作成 const cdkSampleStack = new CdkSampleStack(app, 'CdkSampleStack', {}); // 参照先スタック作成 new CdkSampleStack2(app, 'CdkSampleStack2', { // sampleParameter: cdkSampleStack._sampleParameter, // 参照先でのエクスポートの使用を削除 });
- 参照元スタック
CdkSampleStack
import { aws_ssm, Stack, StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; // 参照元スタック export class CdkSampleStack extends Stack { public readonly _sampleParameter: aws_ssm.StringParameter; constructor(scope: Construct, id: string, props: StackProps) { super(scope, id, props); const sampleParameter = new aws_ssm.StringParameter( this, 'SampleParameter', { parameterName: 'SampleParameter', stringValue: 'SampleValue', } ); // 参照元のエクスポートを削除 // this._sampleParameter = sampleParameter; // this.exportValue(sampleParameter.parameterName); } }
- 参照先スタック
CdkSampleStack2
import { aws_ssm, aws_lambda_nodejs, Stack, StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; interface CdkSampleStack2Props extends StackProps { // sampleParameter: aws_ssm.StringParameter; // 参照先でのエクスポートの使用を削除 } // 参照先スタック export class CdkSampleStack2 extends Stack { constructor(scope: Construct, id: string, props: CdkSampleStack2Props) { super(scope, id, props); // 参照先でのエクスポートの使用を削除 // const sampleFunction = new aws_lambda_nodejs.NodejsFunction( // this, // 'SampleFunction', // { // entry: 'src/sample-function.ts', // environment: { // SAMPLE_PARAMETER_NAME: props.sampleParameter.parameterName, // }, // } // ); // props.sampleParameter.grantRead(sampleFunction); } }
デプロイをしようとすると、参照元スタックで Export <参照先スタック名>:<エクスポート名> cannot be deleted as it is in use by <参照元スタック名>
というエラーが発生し、デプロイが失敗しました。
$ cdk deploy '*' Synthesis time: 2.72s CdkSampleStack CdkSampleStack: deploying... [1/2] CdkSampleStack: creating CloudFormation changeset... CdkSampleStack failed: Error: The stack named CdkSampleStack failed to deploy: UPDATE_ROLLBACK_COMPLETE at FullCloudFormationDeployment.monitorDeployment (/Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:397:10236) at processTicksAndRejections (node:internal/process/task_queues:96:5) at async Object.deployStack2 [as deployStack] (/Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:400:149977) at async /Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:400:135508 Deployment failed: Error: The stack named CdkSampleStack failed to deploy: UPDATE_ROLLBACK_COMPLETE at FullCloudFormationDeployment.monitorDeployment (/Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:397:10236) at processTicksAndRejections (node:internal/process/task_queues:96:5) at async Object.deployStack2 [as deployStack] (/Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:400:149977) at async /Users/wakatsuki.ryuta/.npm-global/lib/node_modules/aws-cdk/lib/index.js:400:135508 The stack named CdkSampleStack failed to deploy: UPDATE_ROLLBACK_COMPLETE
結果として、exportValue を使用した場合でも、参照先での参照と、参照元のエクスポートを、1回のデプロイで削除することはできませんでした。
exportValue の仕様を確認する
ここで CDK のドキュメントで exportValue の仕様を改めて確認してみます。
exportValue は スタックコンストラクトで使えるメソッドです。
ExportValueOptions ではオプションでエクスポートの名前を指定できます。
public exportValue(exportedValue: any, options?: ExportValueOptions): string
exportValue を使うことにより参照元にエクスポートを明示的に作成できます。よって次の記述の通り、参照先から参照を削除する際に、参照元でのエクスポートの存在を保証することが出来るようになります。
One of the uses for this method is to remove the relationship between two Stacks established by automatic cross-stack references. It will temporarily ensure that the CloudFormation Export still exists while you remove the reference from the consuming stack. After that, you can remove the resource and the manual export.
(日本語訳) このメソッドの用途の 1 つは、自動スタック間参照によって確立された 2 つのスタック間の関係を削除することです。 これにより、使用スタックから参照を削除する間、CloudFormation エクスポートがまだ存在することが一時的に確保されます。 その後、リソースと手動エクスポートを削除できます。
まとめ
ここまでの検証と仕様確認の結果、参照元での exportValue の使用の有無によって、参照先で参照を削除した際の挙動はそれぞれ次のようになることが分かりました。
exportValue を使わない場合
exportValue を使わなければ、参照元スタックにあるエクスポートの存在は参照先での参照に依存するようになるため、次のようにデプロイがエラーとなり失敗します。
- デプロイ前の合成で、参照元スタックのエクスポートが「削除」された CloudFormation テンプレートが作成される
- 依存関係の順番から、参照元スタックのデプロイが最初に行われる
- テンプレートに則って参照元スタックにあるエクスポートの削除が試みられるが、実際にはまだ参照先スタックで使用されているため、削除できずにエラーとなる
この挙動は本記事で試したうちの「1. exportValue を使わない場合、参照先でのエクスポートの使用を削除できない」と「3. 参照先での参照と、参照元のエクスポートを、1回のデプロイで削除できるのか?(できなかった)」に該当します。
抜け道としては、削除時に cdkSampleStack.addDependency(cdkSampleStack2);
という作成時とは逆の依存関係を CDK App で定義すれば、参照先スタックが先にデプロイされて参照が削除されるので、デプロイが成功するようになります。しかし同じスタック間で参照の作成と削除を同時にできなくなります。逆の依存関係の定義を削除する掃除用デプロイもどちらにせよ必要となります。
exportValue を使った場合
exportValue を使えば、参照元スタックにあるエクスポートの存在は参照先での参照に依存しなくなり、次のようにデプロイが成功するようになります。
- デプロイ前の合成で、参照元スタックのエクスポートが「保持」された CloudFormation テンプレートが作成される
- 依存関係の順番から、参照元スタックのデプロイが最初に行われる
- 参照元スタックにあるエクスポートは保持される
- 続いて参照先スタックがデプロイされ、参照先の参照が削除される
この挙動は本記事で試したうちの「2. exportValue を使って、参照先でのエクスポートの使用を削除できる」に該当します。
おすすめの記事
今回の内容について詳しく知りたい方へは、以下の記事がおすすめです。
おわりに
AWS CDK のクロススタック参照で、「参照先でのエクスポートの使用」を削除する方法を確認してみました。
最近案件での実装でハマってしまったので、改めて確認してみた次第でした。
参考
以上